Example PRO

import { Button, Navigation, NavigationStack, Script, Text, VStack, SampleBufferView, useState } from "scripting"

let clock: AVSampleBufferRenderSynchronizer | null = null
let video: AVSampleBufferDisplayLayer | null = null
let audio: AVSampleBufferAudioRenderer | null = null
let readers: MediaSampleReader[] = []
let asset: AVAsset | null = null
let cancelled = false
let running: Promise<void> | null = null
let alive = true
let paused = false
const wait = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
function stop() {
  cancelled = true
  for (const reader of readers) reader.dispose()
  readers = []
  clock?.dispose(); clock = null
  video?.dispose(); video = null
  audio?.dispose(); audio = null
  asset?.dispose(); asset = null
}
function Demo() {
  const dismiss = Navigation.useDismiss()
  const [renderer, setRenderer] = useState<AVSampleBufferDisplayLayer | null>(null)
  const [message, setMessage] = useState("Choose a local movie or audio file. No microphone or camera is used.")
  const [busy, setBusy] = useState(false)
  const [isPaused, setPaused] = useState(false)
  const report = (text: string) => { if (alive) setMessage(text) }
  async function play() {
    if (running) return
    const paths = await DocumentPicker.pickFiles({ types: ["public.movie", "public.audio"], allowsMultipleSelection: false })
    if (!paths.length || !alive) return
    stop(); cancelled = false; paused = false; setPaused(false); setBusy(true)
    running = (async () => {
      asset = new AVAsset(paths[0])
      const [vTracks, aTracks] = await Promise.all([asset.loadTracks("video"), asset.loadTracks("audio")])
      if (cancelled) return
      clock = new AVSampleBufferRenderSynchronizer()
      const outputs: { reader: MediaSampleReader; renderer: SampleBufferRenderer; first: MediaSample | null }[] = []
      if (vTracks.length) {
        video = new AVSampleBufferDisplayLayer(); clock.addRenderer(video); setRenderer(video)
        const reader = await MediaSampleReader.create(asset, "video")
        readers.push(reader); outputs.push({ reader, renderer: video, first: await reader.copyNextSampleBuffer() })
      }
      if (aTracks.length) {
        audio = new AVSampleBufferAudioRenderer(); audio.volume = 0.3; clock.addRenderer(audio)
        const reader = await MediaSampleReader.create(asset, "audio")
        readers.push(reader); outputs.push({ reader, renderer: audio, first: await reader.copyNextSampleBuffer() })
      }
      if (cancelled) { outputs.forEach(x => x.first?.dispose()); return }
      const firstTimes = outputs.flatMap(x => x.first?.presentationTime.isNumeric ? [x.first.presentationTime] : [])
      if (!outputs.some(x => x.first)) throw new Error("No playable samples")
      const origin = firstTimes.length ? firstTimes.reduce((a, b) => a.lt(b) ? a : b) : MediaTime.zero()
      const activeClock = clock
      activeClock.setRate(0, origin)
      // Playback borrows the existing shared session. Activate it explicitly without
      // deactivating other script/session owners during cleanup.
      await SharedAudioSession.setActive(true)
      await Promise.all(outputs.map(async output => {
        if (output.first) { try { await output.renderer.enqueue(output.first) } finally { output.first.dispose() } }
      }))
      if (cancelled) return
      activeClock.setRate(1, origin)
      report("Playing. Pause briefly, Resume, or Stop. Restart by choosing the file again.")
      let lastTime = origin.seconds
      await Promise.all(outputs.map(async output => {
        while (!cancelled) {
          while (paused && !cancelled) await wait(50)
          if (cancelled) break
          const sample = await output.reader.copyNextSampleBuffer()
          if (!sample) break
          try {
            const duration = sample.duration.isNumeric ? Math.max(0, sample.duration.seconds) : 0
            if (sample.presentationTime.isNumeric) lastTime = Math.max(lastTime, sample.presentationTime.seconds + duration)
            await output.renderer.enqueue(sample)
          } finally { sample.dispose() }
        }
      }))
      // Reading EOF is not rendering completion. Keep the clock alive until the
      // final timestamp; this does not acknowledge hardware presentation.
      while (!cancelled && activeClock.currentTime.seconds < lastTime) await wait(50)
      if (!cancelled) report("Reached the final media timestamp.")
    })().catch(error => { if (!cancelled) report(String(error)) }).finally(() => {
      stop(); running = null
      if (alive) { setRenderer(null); setBusy(false); setPaused(false) }
    })
    await running
  }
  function togglePause() {
    if (!clock) return
    paused = !paused; clock.setRate(paused ? 0 : 1, clock.currentTime); setPaused(paused)
    report(paused ? "Paused. Resume within 30 seconds if a submission is pending, or Stop." : "Playing")
  }
  return <NavigationStack><VStack spacing={12} padding navigationTitle="Sample Buffer Playback" toolbar={{ cancellationAction: <Button title="Close" action={dismiss} /> }}>
    <Text>{message}</Text>
    {renderer ? <SampleBufferView renderer={renderer} frame={{ height: 240 }} accessibilityLabel="Video playback" /> : <Text>No video loaded</Text>}
    <Button title="Choose and play media" action={play} disabled={busy} />
    <Button title={isPaused ? "Resume" : "Pause"} action={togglePause} disabled={!busy} />
    <Button title="Stop and release" action={() => { stop(); report("Stopped") }} disabled={!busy} />
  </VStack></NavigationStack>
}
async function main() {
  try { await Navigation.present({ element: <Demo /> }) }
  finally { alive = false; stop(); await running; Script.exit() }
}
main()